using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

namespace WMM.Tapioca.Utilities
{
    /// <summary>
    /// Class with normalize and DeepEqualsWithNormalization methods
    /// </summary>
    public static class Normalization
    {
        private static class Xsi
        {
            public static XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";

            public static XName SchemaLocation = xsi + "schemaLocation";
            public static XName NoNamespaceSchemaLocation = xsi + "noNamespaceSchemaLocation";
        }

        /// <summary>
        /// The methos that normalize numeric value.
        /// </summary>
        public static string NormalizeNumericValue(XAttribute attrToNormalize)
        {
            string currAttrValue = attrToNormalize.Value;

            if (!attrToNormalize.Name.NamespaceName.Equals("http://www.xbrl.org/2003/linkbase"))
            {
                if (attrToNormalize != null)
                {
                    if (attrToNormalize.GetSchemaInfo().SchemaType != null)
                    {
                        switch (attrToNormalize.GetSchemaInfo().SchemaType.TypeCode)
                        {
                            case XmlTypeCode.Decimal:
                                currAttrValue = Decimal.Parse(attrToNormalize.Value).ToString();
                                break;
                            case XmlTypeCode.Double:
                                currAttrValue = Double.Parse(attrToNormalize.Value).ToString();
                                break;
                            case XmlTypeCode.Float:
                                currAttrValue = float.Parse(attrToNormalize.Value).ToString();
                                break;
                            case XmlTypeCode.Integer:
                                currAttrValue = Int32.Parse(attrToNormalize.Value).ToString();
                                break;
                            case XmlTypeCode.Boolean:
                                if (attrToNormalize.Value.Equals("1"))
                                {
                                    currAttrValue = "true";
                                }
                                if (attrToNormalize.Value.Equals("0"))
                                {
                                    currAttrValue = "false";
                                }
                                break;
                        }
                    }
                }
            }
            return currAttrValue;
        }

        internal static XDocument Normalize(XDocument source, XmlSchemaSet schema)
        {
            bool havePSVI = false;

            // validate, throw errors, add PSVI information

            if (schema != null)
            {
                source.Validate(schema, null, true);
                havePSVI = true;
            }
            return new XDocument(
                source.Declaration,
                source.Nodes().Select(n =>
                {
                    // Remove comments, processing instructions, and text nodes that are
                    // children of XDocument.  Only white space text nodes are allowed as
                    // children of a document, so we can remove all text nodes.
                    if (n is XComment || n is XProcessingInstruction || n is XText)
                        return null;
                    XElement e = n as XElement;
                    if (e != null)
                        return NormalizeElement(e, havePSVI);
                    return n;
                }
                )
            );
        }


        internal static bool DeepEqualsWithNormalization(XNode xNode1, XNode xNode2,
           bool havePsvi)
        {
            XNode n1 = NormalizeNode(xNode1, havePsvi);
            XNode n2 = NormalizeNode(xNode2, havePsvi);
            return XNode.DeepEquals(n1, n2);
        }


        internal static IEnumerable<XAttribute> NormalizeAttributes(XElement element,
            bool havePsvi)
        {
            return element.Attributes()
                    .Where(a => !a.IsNamespaceDeclaration &&
                        a.Name != Xsi.SchemaLocation &&
                        a.Name != Xsi.NoNamespaceSchemaLocation)
                    .OrderBy(a => a.Name.NamespaceName)
                    .ThenBy(a => a.Name.LocalName)
                    .Select(
                        a =>
                        {
                            if (havePsvi)
                            {
                                if (a.GetSchemaInfo().SchemaType != null)
                                {
                                    var dt = a.GetSchemaInfo().SchemaType.TypeCode;
                                    switch (dt)
                                    {
                                        case XmlTypeCode.Boolean:
                                            return new XAttribute(a.Name, (bool)a);
                                        case XmlTypeCode.DateTime:
                                            return new XAttribute(a.Name, (DateTime)a);
                                        case XmlTypeCode.Decimal:
                                            return new XAttribute(a.Name, ((decimal)a).ToString("G0"));
                                        case XmlTypeCode.Double:
                                            //return a.Value.Contains("INF") ? new XAttribute(a.Name, (string)a) : new XAttribute(a.Name, Math.Abs((double)a));
                                            return a.Value.Contains("INF") ? new XAttribute(a.Name, (string)a) : new XAttribute(a.Name, ((double)a).ToString("G0"));
                                        case XmlTypeCode.Float:
                                            return new XAttribute(a.Name, (float)a);
                                        case XmlTypeCode.HexBinary:
                                        case XmlTypeCode.Language:
                                            return new XAttribute(a.Name,
                                                ((string)a).ToLower());
                                    }
                                }
                            }
                            return a;
                        }
                    );
        }


        private static XNode NormalizeNode(XNode node, bool havePsvi)
        {
            // trim comments and processing instructions from normalized tree
            if (node is XComment || node is XProcessingInstruction)
                return null;
            XElement e = node as XElement;
            if (e != null)
                return NormalizeElement(e, havePsvi);
            // Only thing left is XCData and XText, so clone them
            return node;
        }


        private static XElement NormalizeElement(XElement element, bool havePsvi)
        {
            if (havePsvi)
            {
                var dt = element.GetSchemaInfo();
                if (dt.SchemaType != null)
                {
                    switch (dt.SchemaType.TypeCode)
                    {
                        case XmlTypeCode.Boolean:
                            return new XElement(element.Name,
                                NormalizeAttributes(element, havePsvi),
                                (bool)element);
                        case XmlTypeCode.DateTime:
                            return new XElement(element.Name,
                                NormalizeAttributes(element, havePsvi),
                                (DateTime)element);
                        case XmlTypeCode.Decimal:
                            return new XElement(element.Name,
                                NormalizeAttributes(element, havePsvi),
                                (decimal)element);
                        case XmlTypeCode.Double:
                            return new XElement(element.Name,
                                NormalizeAttributes(element, havePsvi),
                                (double)element);
                        case XmlTypeCode.Float:
                            return new XElement(element.Name,
                                NormalizeAttributes(element, havePsvi),
                                (float)element);
                        case XmlTypeCode.HexBinary:
                        case XmlTypeCode.Language:
                            return new XElement(element.Name,
                                NormalizeAttributes(element, havePsvi),
                                ((string)element).ToLower());
                        default:
                            return new XElement(element.Name,
                                NormalizeAttributes(element, havePsvi),
                                element.Nodes().Select(n => NormalizeNode(n, havePsvi))
                            );
                    }
                }
                else
                {
                    return new XElement(element.Name,
                        NormalizeAttributes(element, havePsvi),
                        element.Nodes().Select(n => NormalizeNode(n, havePsvi))
                    );
                }
            }
            else
            {
                return new XElement(element.Name,
                    NormalizeAttributes(element, havePsvi),
                    element.Nodes().Select(n => NormalizeNode(n, havePsvi))
                );
            }
        }


        static internal bool IsEquivalentTo<T>(this IEnumerable<T> first, IEnumerable<T> second)
        {
            if ((first == null) != (second == null))
                return false;

            if (!object.ReferenceEquals(first, second) && (first != null))
            {
                if (first.Count() != second.Count())
                    return false;

                if ((first.Count() != 0) && HaveMismatchedElement<T>(first, second))
                    return false;
            }

            return true;
        }


        private static bool HaveMismatchedElement<T>(IEnumerable<T> first, IEnumerable<T> second)
        {
            int firstCount;
            int secondCount;

            var firstElementCounts = GetElementCounts<T>(first, out firstCount);
            var secondElementCounts = GetElementCounts<T>(second, out secondCount);

            if (firstCount != secondCount)
                return true;

            foreach (var kvp in firstElementCounts)
            {
                firstCount = kvp.Value;
                secondElementCounts.TryGetValue(kvp.Key, out secondCount);

                if (firstCount != secondCount)
                    return true;
            }

            return false;
        }


        private static Dictionary<T, int> GetElementCounts<T>(IEnumerable<T> enumerable, out int nullCount)
        {
            var dictionary = new Dictionary<T, int>();
            nullCount = 0;

            foreach (T element in enumerable)
            {
                if (element == null)
                {
                    nullCount++;
                }
                else
                {
                    int num;
                    dictionary.TryGetValue(element, out num);
                    num++;
                    dictionary[element] = num;
                }
            }

            return dictionary;
        }


        internal static bool IsNumeric(this XmlSchemaType thisXmlSchemaType)
        {
            switch (thisXmlSchemaType.TypeCode)
            {
                case XmlTypeCode.Decimal:
                case XmlTypeCode.Double:
                case XmlTypeCode.Float:
                case XmlTypeCode.Int:
                case XmlTypeCode.Integer:
                case XmlTypeCode.Long:
                case XmlTypeCode.NegativeInteger:
                case XmlTypeCode.NonNegativeInteger:
                case XmlTypeCode.NonPositiveInteger:
                case XmlTypeCode.PositiveInteger:
                case XmlTypeCode.Short:
                case XmlTypeCode.UnsignedInt:
                case XmlTypeCode.UnsignedLong:
                case XmlTypeCode.UnsignedShort:
                    return true;
                default:
                    return false;
            }
        }

        internal static bool IsNumeric(this XElement element)
        {
            if (element.GetSchemaInfo().SchemaType != null)
            {
                switch (element.GetSchemaInfo().SchemaType.TypeCode)
                {
                    case XmlTypeCode.Decimal:
                    case XmlTypeCode.Double:
                    case XmlTypeCode.Float:
                    case XmlTypeCode.Int:
                    case XmlTypeCode.Integer:
                    case XmlTypeCode.Long:
                    case XmlTypeCode.NegativeInteger:
                    case XmlTypeCode.NonNegativeInteger:
                    case XmlTypeCode.NonPositiveInteger:
                    case XmlTypeCode.PositiveInteger:
                    case XmlTypeCode.Short:
                    case XmlTypeCode.UnsignedInt:
                    case XmlTypeCode.UnsignedLong:
                    case XmlTypeCode.UnsignedShort:
                        return true;
                    default:
                        return false;
                }
            }
            return false;
        }


        internal static bool IsNumeric(this XAttribute attribute)
        {
            if (attribute.GetSchemaInfo().SchemaType != null)
            {
                switch (attribute.GetSchemaInfo().SchemaType.TypeCode)
                {
                    case XmlTypeCode.Decimal:
                    case XmlTypeCode.Double:
                    case XmlTypeCode.Float:
                    case XmlTypeCode.Int:
                    case XmlTypeCode.Integer:
                    case XmlTypeCode.Long:
                    case XmlTypeCode.NegativeInteger:
                    case XmlTypeCode.NonNegativeInteger:
                    case XmlTypeCode.NonPositiveInteger:
                    case XmlTypeCode.PositiveInteger:
                    case XmlTypeCode.Short:
                    case XmlTypeCode.UnsignedInt:
                    case XmlTypeCode.UnsignedLong:
                    case XmlTypeCode.UnsignedShort:
                        return true;
                    default:
                        return false;
                }
            }
            return false;
        }

        internal static bool CompareElementValue(this XElement a, XElement b)
        {
            if (a.GetSchemaInfo().SchemaType != null)
            {
                switch (a.GetSchemaInfo().SchemaType.TypeCode)
                {
                    case XmlTypeCode.Decimal:
                        return decimal.Parse(a.Value) == decimal.Parse(b.Value);
                    case XmlTypeCode.Double:
                        return a.Value.Contains("INF") ? a.Value == b.Value : Double.Parse(a.Value) == Double.Parse(b.Value);
                    case XmlTypeCode.Float:
                        return float.Parse(a.Value) == float.Parse(b.Value);
                    case XmlTypeCode.Int:
                        return int.Parse(a.Value) == int.Parse(b.Value);
                    case XmlTypeCode.Integer:
                    case XmlTypeCode.Long:
                    case XmlTypeCode.NegativeInteger:
                    case XmlTypeCode.NonNegativeInteger:
                    case XmlTypeCode.NonPositiveInteger:
                    case XmlTypeCode.PositiveInteger:
                    case XmlTypeCode.Short:
                    case XmlTypeCode.UnsignedInt:
                    case XmlTypeCode.UnsignedLong:
                    case XmlTypeCode.UnsignedShort:
                        return true;
                    default:
                        return false;
                }
            }
            return false;
        }


        /// <summary>
        /// The methos that normalize elements order.
        /// </summary>
        public static void NormalizeElementsOrder(this XElement node)
        {
            if (node.Elements().Count() > 0)
            {
                node.ReplaceNodes(node.Elements().OrderBy(e => e.Name.NamespaceName)
                                  .ThenBy(e => e.Name.LocalName)
                                  .ThenBy(e => e.Value.Split(':').Length > 1 ? e.Value.Split(':')[1] : e.Value));

                foreach (var childNode in node.Elements())
                {
                    NormalizeElementsOrder(childNode);
                }
            }
        }

    }
}
